/*
 * Copyright 2008 Google Inc. Licensed under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.google.gwt.gen2.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.RequiresResize;

/**
 * A widget that allows the user to select a value within a range of possible
 * values using a sliding bar that responds to mouse events. <h3>Keyboard Events
 * </h3>
 * <p>
 * SliderBarVertical listens for the following key events. Holding down a key
 * will repeat the action until the key is released.
 * <ul class='css'>
 * <li>up arrow - shift up one step</li>
 * <li>down arrow - shift down one step</li>
 * <li>page up - shift up one page</li>
 * <li>page down - shift down one page</li>
 * <li>home - jump to min value</li>
 * <li>end - jump to max value</li>
 * <li>space - jump to middle value</li>
 * </ul>
 * </p>
 */
public class SliderBarVertical extends Composite implements RequiresResize,
    HasValue<Double>, HasValueChangeHandlers<Double> {

  public static final String IE6 = "msie 6.0";
  public static final int DEFAULTPAGESIZE = 25;

  /**
   * The space bar key code.
   */
  private static final int SPACEBAR = 32;

  /*
   * Gets user agent info
   */
  public static native String getUserAgent() /*-{
		return navigator.userAgent.toLowerCase();
  }-*/;

  /**
   * The current value.
   */
  private double curValue;

  /**
   * A bit indicating whether or not the slider is enabled.
   */
  private boolean enabled = true;

  /**
   * The images used with the sliding bar.
   */
  private final SliderBarVerticalResources res;

  /**
   * The knob that slides across the line.
   */
  private final Image knobImage;

  /**
   * The maximum slider value.
   */
  private double maxValue;

  /**
   * The minimum slider value.
   */
  private double minValue;

  /**
   * The number of labels to show.
   */
  private final int numLabels = 0;

  /**
   * The number of tick marks to show.
   */
  private final int numTicks = 0;

  /**
   * The page size value.
   */
  private int pageSize;

  /**
   * A bit indicating whether or not we are currently sliding the slider bar due
   * to mouse events.
   */
  private boolean slidingMouse = false;

  /**
   * An empty constructor to support UI binder.
   * @wbp.parser.constructor
   */
  public SliderBarVertical() {
    this(1, 1000, 1, DEFAULTPAGESIZE);
  }

  /**
   * Create a slider bar.
   * @param minValue2 the minimum value in the range
   * @param maxValue2 the maximum value in the range
   * @param pageSize2 the page size
   */
  public SliderBarVertical(final double minValue2, final double maxValue2,
      final double curValue2, final int pageSize2) {
    this(minValue2, maxValue2, SliderBarVerticalResources.INSTANCE, curValue2,
        pageSize2);
  }

  /**
   * Create a slider bar.
   * @param minValue2 the minimum value in the range
   * @param maxValue2 the maximum value in the range
   * @param images2 the images to use for the slider
   * @param pageSize2 the page size
   */
  public SliderBarVertical(final double minValue2, final double maxValue2,
      final SliderBarVerticalResources res2, final double curValue2,
      final int pageSize2) {
    super();
    res = res2;
    res.sliderBarCss().ensureInjected();
    minValue = minValue2;
    maxValue = maxValue2;

    pageSize = pageSize2;
    curValue = curValue2;

    final FocusPanel panel = new FocusPanel();
    panel.setStyleName(res.sliderBarCss().sliderBarVerticalShell());

    // Create the knob
    knobImage = new Image(res.slider());
    knobImage.setStyleName(res.sliderBarCss().sliderBarVerticalKnob());

    panel.add(knobImage);

    panel.sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS
        | Event.ONMOUSEWHEEL);

    initWidget(panel);
  }

  /**
   * The value change handler.
   * @param handler the handler
   * @return the handle
   */
  @Override
  public final HandlerRegistration addValueChangeHandler(
      final ValueChangeHandler<Double> handler) {
    return addHandler(handler, ValueChangeEvent.getType());
  }

  /**
   * Allows access to the key events.
   * @param keyCode
   */
  public void doKeyAction(final int keyCode) {
    switch (keyCode) {
      case KeyCodes.KEY_HOME:
        setCurrentValue(minValue);
        break;
      case KeyCodes.KEY_END:
        setCurrentValue(maxValue);
        break;
      case KeyCodes.KEY_PAGEUP:
        shiftUp(pageSize);
        break;
      case KeyCodes.KEY_PAGEDOWN:
        shiftDown(pageSize);
        break;
      case KeyCodes.KEY_UP:
        shiftUp(1);
        break;
      case KeyCodes.KEY_DOWN:
        shiftDown(1);
        break;
      case SPACEBAR:
        setCurrentValue(minValue + getTotalRange() / 2);
        break;
      default:
    }
  }

  /**
   * Draw the knob where it is supposed to be relative to the line.
   */
  private void drawKnob() {
    // Abort if not attached
    if (!isAttached()) {
      return;
    }
    // Move the knob to the correct position
    final Element knobElement = knobImage.getElement();
    final int knobHeight = knobElement.getOffsetHeight();

    final int knobTopOffset =
        Math.max(0, (int) (getKnobPercent() * getOffsetHeight()) - knobHeight);
    DOM.setStyleAttribute(knobElement, "top", knobTopOffset + "px");
  }

  /**
   * Return the current value.
   * @return the current value
   */
  public final double getCurrentValue() {
    return curValue;
  }

  /**
   * Get the percentage of the knob's position relative to the size of the line.
   * The return value will be between 0.0 and 1.0.
   * @return the current percent complete
   */
  protected final double getKnobPercent() {
    // If we have no range
    if (maxValue <= minValue) {
      return 0;
    }

    // Calculate the relative progress
    final double percent = (curValue - minValue) / (maxValue - minValue);
    return Math.max(0.0, Math.min(1.0, percent));
  }

  /**
   * Calculate the knob's position in coordinates.
   * @return the knob's position
   */
  private int getKnobPosition() {
    return knobImage.getAbsoluteTop();
  }

  /**
   * Return the max value.
   * @return the max value
   */
  public final double getMaxValue() {
    return maxValue;
  }

  /**
   * Return the minimum value.
   * @return the minimum value
   */
  public final double getMinValue() {
    return minValue;
  }

  /**
   * Return the number of labels.
   * @return the number of labels
   */
  public final int getNumLabels() {
    return numLabels;
  }

  /**
   * Return the number of ticks.
   * @return the number of ticks
   */
  public final int getNumTicks() {
    return numTicks;
  }

  /**
   * Retrieve the page size.
   * @return the page size
   */
  public final int getPageSize() {
    return pageSize;
  }

  /**
   * Return the total range between the minimum and maximum values.
   * @return the total range
   */
  public final double getTotalRange() {
    if (minValue > maxValue) {
      return 0;
    } else {
      return maxValue - minValue;
    }
  }

  @Override
  public final Double getValue() {
    return curValue;
  }

  /**
   * @return Gets whether this widget is enabled
   */
  public final boolean isEnabled() {
    return enabled;
  }

  /**
   * Listen for events that will move the knob.
   * @param event the event that occurred
   */
  @Override
  public final void onBrowserEvent(final Event event) {
    super.onBrowserEvent(event);
    if (enabled) {
      switch (DOM.eventGetType(event)) {

        case Event.ONBLUR:
          GWT.log("blur");

          if (slidingMouse) {
            slidingMouse = false;
            slideKnob(event, true);
          }
          break;

        // Mousewheel events
        case Event.ONMOUSEWHEEL:
          GWT.log("onmousewheel");
          onWheelEvent(event);
          break;

        // Shift left or right on key press
        case Event.ONKEYDOWN:
          onKeyDownEvent(event);
          break;

        // Mouse Events
        case Event.ONMOUSEDOWN:
          GWT.log("onmousedown");
          if (sliderClicked(event)) {
            slidingMouse = true;
            DOM.setCapture(getElement());
            DOM.eventPreventDefault(event);
            slideKnob(event, false);
          } else {
            pageKnob(event);
          }
          break;
        case Event.ONMOUSEUP:
          GWT.log("onmouseup");
          DOM.releaseCapture(getElement());
          if (slidingMouse) {
            slideKnob(event, true);
            slidingMouse = false;
          }
          break;
        case Event.ONMOUSEMOVE:
          GWT.log("onmousemove");
          if (slidingMouse) {
            slideKnob(event, false);
          }
          break;
        default:
      }
    }
  }

  public void onKeyDownEvent(final Event event) {
    GWT.log("onkeydown");
    DOM.eventPreventDefault(event);
    if (slidingMouse) {
      slidingMouse = false;
    }
    doKeyAction(DOM.eventGetKeyCode(event));
  }

  /**
   * Handle the resize event.
   */
  @Override
  public void onResize() {
    if (isAttached()) {
      drawKnob();
    }
  }

  public final void onWheelEvent(final Event event) {
    event.preventDefault();
    final int deltaY = event.getMouseWheelVelocityY();
    if (deltaY > 0) {
      shiftDown(deltaY);
    } else {
      shiftUp(Math.abs(deltaY));
    }
  }

  /**
   * Perform a page action.
   * @param event the event
   */
  private void pageKnob(final Event event) {
    final int y = DOM.eventGetClientY(event);
    if (y > 0) {
      if (y < getKnobPosition()) {
        shiftUp(pageSize);
      } else {
        shiftDown(pageSize);
      }
    }
  }

  /**
   * Reset the progress to constrain the progress to the current range and
   * redraw the knob as needed.
   * @param fireEvent
   */
  private void resetCurrentValue(final boolean fireEvent) {
    setCurrentValue(getCurrentValue(), fireEvent);
  }

  /**
   * Set the current value and fire the onValueChange event.
   * @param curValue2 the current value
   */
  public final void setCurrentValue(final double curValue2) {
    setCurrentValue(curValue2, true);
  }

  /**
   * Set the current value and optionally fire the onValueChange event.
   * @param curValue2 the current value
   * @param fireEvent fire the onValue change event if true
   */
  public final void setCurrentValue(final double curValue2,
      final boolean fireEvent) {
    // Confine the value to the range
    curValue = Math.max(minValue, Math.min(maxValue, curValue2));

    // Redraw the knob
    drawKnob();

    // Fire the ValueChangeEvent
    if (fireEvent) {
      // keyTimer.cancel();
      ValueChangeEvent.fire(this, curValue);
    }
  }

  /**
   * Sets whether this widget is enabled.
   * @param enabled2 true to enable the widget, false to disable it
   */
  public final void setEnabled(final boolean enabled2) {

    enabled = enabled2;
    if (enabled) {
      knobImage.setResource(res.slider());
    } else {
      knobImage.setResource(res.sliderDisabled());
    }
  }

  /**
   * Set the max value.
   * @param maxValue2 the max value
   * @param fireEvent
   */
  public final void setMaxValue(final double maxValue2) {
    maxValue = maxValue2;
    resetCurrentValue(true);
  }

  /**
   * Set the max value.
   * @param maxValue2 the max value
   * @param fireEvent
   */
  public final void setMaxValue(final double maxValue2, final boolean fireEvent) {
    maxValue = maxValue2;
    resetCurrentValue(fireEvent);
  }

  /**
   * Set the minimum value.
   * @param minValue2 the current value
   */
  public final void setMinValue(final double minValue2) {
    minValue = minValue2;
    resetCurrentValue(true);
  }

  /**
   * Set the page size.
   * @param pageSize2 the amount to page.
   */
  public final void setPageSize(final int pageSize2) {
    pageSize = pageSize2;
  }

  /**
   * Set the value of the slider in real terms.
   * @param value the value
   */
  @Override
  public final void setValue(final Double value) {
    setCurrentValue(value, false);
  }

  /**
   * Set the value of the slider in real terms.
   * @param value the value
   * @param fireEvent whether to fire an event
   */
  @Override
  public final void setValue(final Double value, final boolean fireEvent) {
    setCurrentValue(value, fireEvent);
  }

  /**
   * Shift to the right (greater value).
   * @param numSteps the number of steps to shift
   */
  public final void shiftDown(final int numSteps) {
    setCurrentValue(getCurrentValue() + numSteps);
  }

  /**
   * Shift to the left (smaller value).
   * @param numSteps the number of steps to shift
   */
  public final void shiftUp(final int numSteps) {
    setCurrentValue(getCurrentValue() - numSteps);
  }

  /**
   * Slide the knob to a new location.
   * @param event the mouse event
   * @param fireEvent whether or not to just slide the knob
   */
  private void slideKnob(final Event event, final boolean fireEvent) {
    final int y = DOM.eventGetClientY(event);
    if (y > 0) {
      final int lineTop = getAbsoluteTop();
      final double percent = (double) (y - lineTop) / getOffsetHeight() * 1.0;
      setCurrentValue(getTotalRange() * percent + minValue, fireEvent);
    }
  }

  /**
   * Checks whether the slider was clicked or not.
   * @param event the mouse event
   * @return whether the slider was clicked
   */
  private boolean sliderClicked(final Event event) {
    boolean sliderClicked = false;

    if (getUserAgent().contains(IE6)) {
      if (DOM.eventGetTarget(event).equals(
          knobImage.getElement().getFirstChild())) {
        sliderClicked = true;
      }
    } else if (DOM.eventGetTarget(event).equals(knobImage.getElement())) {
      sliderClicked = true;
    }

    return sliderClicked;
  }

}